--[[
Generates lua-ls annotations for lsp
USAGE:
nvim -l scripts/lsp_types.lua gen --runtime/lua/vim/lsp/types/protocol.lua
--]]

local M = {}

local function tofile(fname, text)
  local f = io.open(fname, 'w')
  if not f then
    error(('failed to write: %s'):format(f))
  else
    f:write(text)
    f:close()
  end
end

function M.gen(opt)
  if vim.loop.fs_stat('./lsp.json') then
    vim.fn.delete('./lsp.json')
  end
  vim.fn.system({
    'curl',
    'https://raw.githubusercontent.com/microsoft/lsprotocol/main/generator/lsp.json',
    '-o',
    './lsp.json',
  })
  local protocol = vim.fn.json_decode(vim.fn.readfile('./lsp.json'))
  vim.fn.delete('./lsp.json')
  local output_file = opt[1]
  protocol = protocol or {}
  local output = {
    '--[[',
    'This file is autogenerated from scripts/lsp_types.lua',
    'Regenerate:',
    [=[nvim -l scripts/lsp_types.lua gen --runtime/lua/vim/lsp/types/protocol.lua]=],
    '--]]',
    '',
    '---@alias lsp.null nil',
    '---@alias uinteger integer',
    '---@alias lsp.decimal number',
    '---@alias lsp.DocumentUri string',
    '---@alias lsp.URI string',
    '---@alias lsp.LSPObject table<string, lsp.LSPAny>',
    '---@alias lsp.LSPArray lsp.LSPAny[]',
    '---@alias lsp.LSPAny lsp.LSPObject|lsp.LSPArray|string|number|boolean|nil',
    '',
  }

  local anonymous_num = 0

  local anonym_classes = {}

  local simple_types = {
    'string',
    'boolean',
    'integer',
    'uinteger',
    'decimal',
  }

  local function parse_type(type)
    if type.kind == 'reference' or type.kind == 'base' then
      if vim.tbl_contains(simple_types, type.name) then
        return type.name
      end
      return 'lsp.' .. type.name
    elseif type.kind == 'array' then
      return parse_type(type.element) .. '[]'
    elseif type.kind == 'or' then
      local val = ''
      for _, item in ipairs(type.items) do
        val = val .. parse_type(item) .. '|'
      end
      val = val:sub(0, -2)
      return val
    elseif type.kind == 'stringLiteral' then
      return '"' .. type.value .. '"'
    elseif type.kind == 'map' then
      return 'table<' .. parse_type(type.key) .. ', ' .. parse_type(type.value) .. '>'
    elseif type.kind == 'literal' then
      -- can I use ---@param disabled? {reason: string}
      -- use | to continue the inline class to be able to add docs
      -- https://github.com/LuaLS/lua-language-server/issues/2128
      anonymous_num = anonymous_num + 1
      local anonym = { '---@class anonym' .. anonymous_num }
      for _, field in ipairs(type.value.properties) do
        if field.documentation then
          field.documentation = field.documentation:gsub('\n', '\n---')
          anonym[#anonym + 1] = '---' .. field.documentation
        end
        anonym[#anonym + 1] = '---@field '
          .. field.name
          .. (field.optional and '?' or '')
          .. ' '
          .. parse_type(field.type)
      end
      anonym[#anonym + 1] = ''
      for _, line in ipairs(anonym) do
        anonym_classes[#anonym_classes + 1] = line
      end
      return 'anonym' .. anonymous_num
    elseif type.kind == 'tuple' then
      local tuple = '{ '
      for i, value in ipairs(type.items) do
        tuple = tuple .. '[' .. i .. ']: ' .. parse_type(value) .. ', '
      end
      -- remove , at the end
      tuple = tuple:sub(0, -3)
      return tuple .. ' }'
    end
    vim.print(type)
    return ''
  end

  for _, structure in ipairs(protocol.structures) do
    if structure.documentation then
      structure.documentation = structure.documentation:gsub('\n', '\n---')
      output[#output + 1] = '---' .. structure.documentation
    end
    if structure.extends then
      local class_string = '---@class lsp.'
        .. structure.name
        .. ': '
        .. parse_type(structure.extends[1])
      for _, mixin in ipairs(structure.mixins or {}) do
        class_string = class_string .. ', ' .. parse_type(mixin)
      end
      output[#output + 1] = class_string
    else
      output[#output + 1] = '---@class lsp.' .. structure.name
    end
    for _, field in ipairs(structure.properties or {}) do
      if field.documentation then
        field.documentation = field.documentation:gsub('\n', '\n---')
        output[#output + 1] = '---' .. field.documentation
      end
      output[#output + 1] = '---@field '
        .. field.name
        .. (field.optional and '?' or '')
        .. ' '
        .. parse_type(field.type)
    end
    output[#output + 1] = ''
  end

  for _, enum in ipairs(protocol.enumerations) do
    if enum.documentation then
      enum.documentation = enum.documentation:gsub('\n', '\n---')
      output[#output + 1] = '---' .. enum.documentation
    end
    local enum_type = '---@alias lsp.' .. enum.name
    for _, value in ipairs(enum.values) do
      enum_type = enum_type
        .. '\n---| '
        .. (type(value.value) == 'string' and '"' .. value.value .. '"' or value.value)
        .. ' # '
        .. value.name
    end
    output[#output + 1] = enum_type
    output[#output + 1] = ''
  end

  for _, alias in ipairs(protocol.typeAliases) do
    if alias.documentation then
      alias.documentation = alias.documentation:gsub('\n', '\n---')
      output[#output + 1] = '---' .. alias.documentation
    end
    if alias.type.kind == 'or' then
      local alias_type = '---@alias lsp.' .. alias.name .. ' '
      for _, item in ipairs(alias.type.items) do
        alias_type = alias_type .. parse_type(item) .. '|'
      end
      alias_type = alias_type:sub(0, -2)
      output[#output + 1] = alias_type
    else
      output[#output + 1] = '---@alias lsp.' .. alias.name .. ' ' .. parse_type(alias.type)
    end
    output[#output + 1] = ''
  end

  for _, line in ipairs(anonym_classes) do
    output[#output + 1] = line
  end

  tofile(output_file, table.concat(output, '\n'))
end

local opt = {}

local index = 1
for _, a in ipairs(arg) do
  if vim.startswith(a, '--') then
    local name = a:sub(3)
    opt[index] = name
    index = index + 1
  end
end

for _, a in ipairs(arg) do
  if M[a] then
    M[a](opt)
  end
end

return M
